package sketch.specs;

import java.util.LinkedList;
import java.util.List;

import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

import sketch.ast.ASTUtils;
import sketch.main.SketchTestOptions;
import sketch.util.Checker;
import sketch.util.Globals;

public class SketchedMethodDeclaration {

	public static String replic_sep = "_rep_";
	
	public final MethodDeclaration method_declaration;
	
	public final List<VariableDeclarationStatement> valueDeclarationStatements;
	public final List<SketchedBlock> childrenBlocks = new LinkedList<SketchedBlock>();
	private final List<SketchedBlock> allSubBlocks = new LinkedList<SketchedBlock>();
	
	public SketchedMethodDeclaration(MethodDeclaration method_declaration) {
		Checker.checkNull(method_declaration, "The method declaration input could not be null.");
		this.method_declaration = method_declaration;
		//need to fetch all variable declaration statements here
		this.valueDeclarationStatements = 
			SpecValueDeclarationFinder.findVariableDeclarationStatements(this.method_declaration.getBody());
	}
	
	//XXX be aware it is easy to miss one branch on whether childrenBlocks is empty or not
	public List<MethodDeclaration> getMethodsAfterExpansion() {
		List<MethodDeclaration> initMethods = new LinkedList<MethodDeclaration>();
		initMethods.add(ASTUtils.deepClone(method_declaration));
		if(this.childrenBlocks.isEmpty()) {
			return this.expandValues();
			//return initMethods;
		}
		//start to replace
		String methodName = this.method_declaration.getName().getIdentifier();
		int replicate_count = 0;
		//XXX the replication algorithm below could be problematic!
		List<MethodDeclaration> processingMethods = new LinkedList<MethodDeclaration>();
//		List<MethodDeclaration> retMethods = new LinkedList<MethodDeclaration>();
		processingMethods.addAll(initMethods);
		//System.out.println("no of children blocks: " + this.childrenBlocks.size());
		for(SketchedBlock child : this.childrenBlocks) {
			List<Block> childBlocks = child.expandAll();
			//System.out.println("no of expanded blocks for the child: " + childBlocks.size());
			Block childRealBlock = child.realBlock;
			List<MethodDeclaration> fringe = new LinkedList<MethodDeclaration>();
			for(MethodDeclaration method : processingMethods) {
				MethodDeclaration copyMethod = ASTUtils.deepClone(method);
				Block wholeBlock = copyMethod.getBody();
				//replace the child block
				for(Block newChildBlock : childBlocks) {
					SourceExpansionVisitor visitor = new SourceExpansionVisitor(wholeBlock, childRealBlock, newChildBlock);
					Block newReplacedBlock = visitor.getReplacedBlock();
					//create a method declaration here
					MethodDeclaration newMethod = copyMethod.getAST().newMethodDeclaration();
					//newMethod.modifiers().addAll(this.method_declaration.modifiers());
					newMethod.getName().setIdentifier(methodName  + replic_sep + (replicate_count++));
					newMethod.setBody(newReplacedBlock);
					fringe.add(newMethod);
				}
			}
			processingMethods.clear();
			processingMethods.addAll(fringe);
		}
		//remove the methods written in the given test specification
		processingMethods.removeAll(initMethods);
		this.addOUnitAnnotation(processingMethods);
		
		//replicate using for different identifier
		//return processingMethods;
		//List<MethodDeclaration> methodsAfterExpandingIds = this.expandIdentifierBlocks(processingMethods);
		
		List<MethodDeclaration> retMethods = new LinkedList<MethodDeclaration>();
		
		if(!this.valueDeclarationStatements.isEmpty()) {
//			System.out.println(this.valueDeclarationStatements);
//			System.out.println(methodsAfterExpandingIds);
			for(MethodDeclaration processedMethod : processingMethods) {
				SketchedMethodDeclaration sd = new SketchedMethodDeclaration(processedMethod);
				retMethods.addAll(sd.expandValues());
			}
		} else {
			retMethods.addAll(processingMethods);
		}
		
		//System.out.println(retMethods);
		
		return retMethods;
	}
	
	List<MethodDeclaration> expandValues() {
		List<MethodDeclaration> method_declarations = new LinkedList<MethodDeclaration>();
		List<Block> currentBlocks = new LinkedList<Block>();
		currentBlocks.add(ASTUtils.deepClone(this.method_declaration.getBody()));
		//expand with value
		List<Block> newBlocks = SketchedBlock.expandWithValues(currentBlocks, this.valueDeclarationStatements);
		//System.out.println(newBlocks);
		int count = 0;
		for(Block block : newBlocks) {
			MethodDeclaration m = this.method_declaration.getAST().newMethodDeclaration();
			block.delete();
			m.setBody(block);
			//add to the return list
			method_declarations.add(m);
			//set the name
			String methodName = this.method_declaration.getName().getIdentifier();
			if(methodName.indexOf(replic_sep) == -1) {
				methodName = methodName + replic_sep;
			}
			m.getName().setIdentifier(methodName + (count++));
			//make it public
			ASTUtils.makeMethodPublic(m);
		}
		
		return method_declarations;
	}
	
	List<MethodDeclaration> expandIdentifierBlocks(List<MethodDeclaration> method_declarations) {
		boolean hasIdBlock = false;
		for(SketchedBlock b : this.childrenBlocks) {
			if (b instanceof SketchedIdentifierBlock) {
				hasIdBlock = true;
				break;
			}
		}
		if(!hasIdBlock) {
			return method_declarations;
		}
		//get the return methods
		List<MethodDeclaration> retMethods = new LinkedList<MethodDeclaration>();
		for(MethodDeclaration method_decl : method_declarations) {
			List<Block> blockToExpand = new LinkedList<Block>();
			blockToExpand.add(method_decl.getBody());
			List<Block> blocksAfterExpanded = IdentifierBlockExpander.expandOnIdentifier(method_decl.getBody(),
					this.childrenBlocks, blockToExpand);
			int count = 1;
			for(Block blockAfterExpand : blocksAfterExpanded) {
			  blockAfterExpand.delete();
			  MethodDeclaration newMethod = method_decl.getAST().newMethodDeclaration();
			  newMethod.getName().setIdentifier(method_decl.getName().getIdentifier() + (count++));
			  newMethod.setBody(blockAfterExpand);
			  retMethods.add(newMethod);
			}
		}
		return retMethods;
	}
	
	void addOUnitAnnotation(List<MethodDeclaration> methods) {
		for(MethodDeclaration method : methods) {
			List<IExtendedModifier> modifiers = method.modifiers();
			//create an annotation
//			MarkerAnnotation annotation = method.getAST().newMarkerAnnotation();
//			annotation.setTypeName(method.getAST().newName("sketch.ounit.OUnit"));
//			modifiers.add(0, annotation);
			ASTUtils.addOUnitAnnotation(method);
//			xx
		}
	}
	
	void addSubBlock(SketchedBlock block) {
		this.allSubBlocks.add(block);
	}
	
	void buildSubBlockHierarchy() {
		if(allSubBlocks.isEmpty()) {
			return;
		}
		//the rough algorithm psedo code
		//find the top level blocks, and their sub-blocks
		List<SketchedBlock> firstLevel = SketchedBlock.findFirstLevelBlocks(allSubBlocks);
		//add to the childrenBlocks
		this.childrenBlocks.addAll(firstLevel);
		//the remaining blocks
		List<SketchedBlock> remainings = new LinkedList<SketchedBlock>();
		for(SketchedBlock b : allSubBlocks) {
			if(!firstLevel.contains(b)) {
				remainings.add(b);
			}
		}
		Checker.checkTrue((remainings.size() + this.childrenBlocks.size())== allSubBlocks.size(),
				"The number of remaining is: " + remainings.size() + ", the number of children is: "
				+ this.childrenBlocks.size() + ", the number of all sub blocks: " + allSubBlocks.size());
		int count = 0; //for correctness checking
		for(SketchedBlock child : this.childrenBlocks) {
			List<SketchedBlock> subBlocks = SketchedBlock.findSubBlocks(child, remainings);
			count = count + subBlocks.size();
			child.addSubBlocks(subBlocks);
		}
		Checker.checkTrue(count == remainings.size(), "The count: " + count + " does not equal to " +
				"the remaining block size: " + remainings.size());
		//call build hierarchical in the sub blocks
		for(SketchedBlock child : this.childrenBlocks) {
			child.buildSubBlockHierarchy();
		}
	}
	
	void buildParentRelations() {
		//throw new RuntimeException("Unimplemented!");
		List<SketchedBlock> topLevels = this.childrenBlocks;
		for(SketchedBlock topLevel : topLevels) {
			topLevel.setSketchedBlockAndMethod(null, this);
		}
		//move down one level by one level
		List<SketchedBlock> fringe = new LinkedList<SketchedBlock>();
		fringe.addAll(topLevels);
		while(!fringe.isEmpty()) {
			SketchedBlock block = fringe.remove(0);
			Checker.checkTrue(block.isParentSetted(), "The parent should already been set!");
			for(SketchedBlock child : block.childrenBlocks) {
				child.setSketchedBlockAndMethod(block, this);
				fringe.add(child);
			}
		}
	}
	
	public int numOfSubBlocks() {
		return this.allSubBlocks.size();
	}
	
	public int numOfChildren() {
		return this.childrenBlocks.size();
	}
	
	public List<SketchedBlock> getAllSubBlocks() {
		return this.allSubBlocks;
	}
	
	public void printHierarchy() {
		StringBuilder sb = new StringBuilder();
		for(SketchedBlock block : this.childrenBlocks) {
			sb.append(this.appendSketchedBlock(0, block));
		}
		if(SketchTestOptions.verbose) {
		  System.out.println("----");
		  System.out.println(sb.toString());
		}
	}
	
	@Override
	public String toString() {
		return this.method_declaration.toString();
	}
	
	private String appendSketchedBlock(int level, SketchedBlock block) {
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < level; i++) {
			sb.append("  ");
		}
		sb.append(block.getClass() + "(" + block.getStartPosition() + ", " + block.getEndPosition()+ ")" +
				"+ children: " + block.childrenBlocks.size() + ", parent: " +
				(block.parentBlock == null? " null! " : ( " (" + block.parentBlock.getStartPosition() + ", " + block.parentBlock.getEndPosition() + ")") ));
		sb.append(Globals.lineSep);
		for(SketchedBlock child : block.childrenBlocks) {
			sb.append(appendSketchedBlock(level + 1, child));
		}
		return sb.toString();
	}
}